JavaScript Yineleyici Protokolü'nü anlamak ve uygulamak için kapsamlı bir kılavuz; gelişmiş veri işleme için özel yineleyiciler oluşturmanızı sağlar.
JavaScript Yineleyici Protokolü ve Özel Yineleyicilerin Gizemini Çözmek
JavaScript'in Yineleyici Protokolü, veri yapılarında gezinmek için standart bir yol sağlar. Bu protokolü anlamak, geliştiricilere diziler ve string'ler gibi dahili yinelenebilirlerle verimli bir şekilde çalışma ve belirli veri yapılarına ve uygulama gereksinimlerine göre uyarlanmış kendi özel yinelenebilirlerini oluşturma gücü verir. Bu kılavuz, Yineleyici Protokolü'nün ve özel yineleyicilerin nasıl uygulanacağının kapsamlı bir incelemesini sunar.
Yineleyici Protokolü Nedir?
Yineleyici Protokolü, bir nesnenin nasıl yinelenebileceğini, yani elemanlarına sırayla nasıl erişilebileceğini tanımlar. İki bölümden oluşur: Yinelenebilir (Iterable) protokolü ve Yineleyici (Iterator) protokolü.
Yinelenebilir (Iterable) Protokolü
Bir nesne, Symbol.iterator
anahtarına sahip bir metodu varsa Yinelenebilir (Iterable) kabul edilir. Bu metod, Yineleyici (Iterator) protokolüne uyan bir nesne döndürmelidir.
Özünde, yinelenebilir bir nesne kendisi için bir yineleyici oluşturmayı bilir.
Yineleyici (Iterator) Protokolü
Yineleyici (Iterator) protokolü, bir diziden değerlerin nasıl alınacağını tanımlar. Bir nesne, iki özelliğe sahip bir nesne döndüren bir next()
metoduna sahipse yineleyici olarak kabul edilir:
value
: Dizideki bir sonraki değer.done
: Yineleyicinin dizinin sonuna ulaşıp ulaşmadığını belirten bir boolean değeri. Eğerdone
true
ise,value
özelliği atlanabilir.
next()
metodu, Yineleyici protokolünün iş yükünü çeken kısmıdır. next()
'e yapılan her çağrı, yineleyiciyi ilerletir ve dizideki bir sonraki değeri döndürür. Tüm değerler döndürüldüğünde, next()
, done
özelliği true
olarak ayarlanmış bir nesne döndürür.
Dahili Yinelenebilirler
JavaScript, doğası gereği yinelenebilir olan birkaç dahili veri yapısı sunar. Bunlar şunları içerir:
- Diziler
- String'ler
- Map'ler
- Set'ler
- Bir fonksiyonun Arguments nesnesi
- TypedArray'ler
Bu yinelenebilirler, for...of
döngüsü, spread sözdizimi (...
) ve Yineleyici Protokolü'ne dayanan diğer yapılarla doğrudan kullanılabilir.
Dizilerle Örnek:
const myArray = ["elma", "muz", "kiraz"];
for (const item of myArray) {
console.log(item); // Çıktı: elma, muz, kiraz
}
String'lerle Örnek:
const myString = "Merhaba";
for (const char of myString) {
console.log(char); // Çıktı: M, e, r, h, a, b, a
}
for...of
Döngüsü
for...of
döngüsü, yinelenebilir nesneler üzerinde yineleme yapmak için güçlü bir yapıdır. Yineleyici Protokolü'nün karmaşıklıklarını otomatik olarak yönetir, bu da bir dizideki değerlere erişimi kolaylaştırır.
for...of
döngüsünün sözdizimi şöyledir:
for (const element of iterable) {
// Her bir eleman için çalıştırılacak kod
}
for...of
döngüsü, yinelenebilir nesneden yineleyiciyi alır (Symbol.iterator
kullanarak) ve done
true
olana kadar yineleyicinin next()
metodunu tekrar tekrar çağırır. Her yinelemede, element
değişkenine next()
tarafından döndürülen value
özelliği atanır.
Özel Yineleyiciler Oluşturma
JavaScript dahili yinelenebilirler sağlasa da, Yineleyici Protokolü'nün gerçek gücü, kendi veri yapılarınız için özel yineleyiciler tanımlama yeteneğinde yatar. Bu, verilerinizin nasıl gezileceğini ve erişileceğini kontrol etmenizi sağlar.
İşte özel bir yineleyici nasıl oluşturulur:
- Özel veri yapınızı temsil eden bir sınıf veya nesne tanımlayın.
- Sınıfınızda veya nesnenizde
Symbol.iterator
metodunu uygulayın. Bu metod bir yineleyici nesnesi döndürmelidir. - Yineleyici nesnesi,
value
vedone
özelliklerine sahip bir nesne döndüren birnext()
metoduna sahip olmalıdır.
Örnek: Basit bir Aralık için Yineleyici Oluşturma
Bir sayı aralığını temsil eden Range
adında bir sınıf oluşturalım. Aralıktaki sayılar üzerinde yineleme yapmaya izin vermek için Yineleyici Protokolü'nü uygulayacağız.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let currentValue = this.start;
const that = this; // Yineleyici nesnesi içinde kullanmak için 'this'i yakala
return {
next() {
if (currentValue <= that.end) {
return {
value: currentValue++,
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Çıktı: 1, 2, 3, 4, 5
}
Açıklama:
Range
sınıfı, kurucusundastart
veend
değerlerini alır.Symbol.iterator
metodu bir yineleyici nesnesi döndürür. Bu yineleyici nesnesinin kendi durumu (currentValue
) ve birnext()
metodu vardır.next()
metodu,currentValue
'nun aralık içinde olup olmadığını kontrol eder. Eğer içindeyse, mevcut değeri vedone
değerinifalse
olarak ayarlayan bir nesne döndürür. Ayrıca bir sonraki yineleme içincurrentValue
'yu artırır.currentValue
,end
değerini aştığında,next()
metodudone
değeritrue
olarak ayarlanmış bir nesne döndürür.that = this
kullanımına dikkat edin.next()
metodu farklı bir kapsamda (for...of
döngüsü tarafından) çağrıldığı için,next()
içindekithis
,Range
örneğine başvurmaz. Bunu çözmek için,this
değerini (Range
örneğini)next()
kapsamının dışındathat
içinde yakalarız ve ardındannext()
içindethat
'ı kullanırız.
Örnek: Bir Bağlı Liste için Yineleyici Oluşturma
Başka bir örnek düşünelim: bir bağlı liste veri yapısı için bir yineleyici oluşturma. Bir bağlı liste, her bir düğümün bir değer ve listedeki bir sonraki düğüme bir referans (işaretçi) içerdiği bir düğüm dizisidir. Listenin son düğümü null'a (veya undefined'a) bir referansa sahiptir.
class LinkedListNode {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
}
append(value) {
const newNode = new LinkedListNode(value);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {
value: value,
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
};
}
}
// Örnek Kullanım:
const myList = new LinkedList();
myList.append("Londra");
myList.append("Paris");
myList.append("Tokyo");
for (const city of myList) {
console.log(city); // Çıktı: Londra, Paris, Tokyo
}
Açıklama:
LinkedListNode
sınıfı, bağlı listedeki tek bir düğümü temsil eder, birvalue
ve bir sonraki düğüme bir referans (next
) saklar.LinkedList
sınıfı, bağlı listenin kendisini temsil eder. Listenin ilk düğümünü gösteren birhead
özelliğine sahiptir.append()
metodu listenin sonuna yeni düğümler ekler.Symbol.iterator
metodu bir yineleyici nesnesi oluşturur ve döndürür. Bu yineleyici, ziyaret edilen mevcut düğümü (current
) takip eder.next()
metodu, mevcut bir düğüm olup olmadığını kontrol eder (current
null değilse). Varsa, mevcut düğümden değeri alır,current
işaretçisini bir sonraki düğüme ilerletir ve değeri vedone: false
içeren bir nesne döndürür.current
null olduğunda (listenin sonuna ulaştığımız anlamına gelir),next()
metodudone: true
içeren bir nesne döndürür.
Üreteç (Generator) Fonksiyonları
Üreteç fonksiyonları, yineleyiciler oluşturmak için daha kısa ve zarif bir yol sağlar. Talep üzerine değerler üretmek için yield
anahtar kelimesini kullanırlar.
Bir üreteç fonksiyonu, function*
sözdizimi kullanılarak tanımlanır.
Örnek: Bir Üreteç Fonksiyonu Kullanarak Yineleyici Oluşturma
Range
yineleyicisini bir üreteç fonksiyonu kullanarak yeniden yazalım:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Çıktı: 1, 2, 3, 4, 5
}
Açıklama:
Symbol.iterator
metodu artık bir üreteç fonksiyonudur (*
işaretine dikkat edin).- Üreteç fonksiyonunun içinde, sayı aralığı üzerinde yineleme yapmak için bir
for
döngüsü kullanırız. yield
anahtar kelimesi, üreteç fonksiyonunun yürütülmesini duraklatır ve mevcut değeri (i
) döndürür. Yineleyicininnext()
metodunun bir sonraki çağrısında, yürütme kaldığı yerden (yield
ifadesinden sonra) devam eder.- Döngü bittiğinde, üreteç fonksiyonu dolaylı olarak
{ value: undefined, done: true }
döndürür, bu da yinelemenin sonunu işaret eder.
Üreteç fonksiyonları, next()
metodunu ve done
bayrağını otomatik olarak yöneterek yineleyici oluşturmayı basitleştirir.
Örnek: Fibonacci Dizisi Üreteci
Üreteç fonksiyonlarını kullanmanın bir başka harika örneği de Fibonacci dizisini oluşturmaktır:
function* fibonacciSequence() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b]; // Eşzamanlı güncelleme için yapı bozma ataması
}
}
const fibonacci = fibonacciSequence();
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // Çıktı: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
Açıklama:
fibonacciSequence
fonksiyonu bir üreteç fonksiyonudur.- Fibonacci dizisindeki ilk iki sayıya (0 ve 1) iki değişken,
a
veb
, başlatır. while (true)
döngüsü sonsuz bir dizi oluşturur.yield a
ifadesi,a
'nın mevcut değerini üretir.[a, b] = [b, a + b]
ifadesi, yapı bozma ataması kullanaraka
veb
'yi dizideki sonraki iki sayıya eşzamanlı olarak günceller.fibonacci.next().value
ifadesi, üreteçten bir sonraki değeri alır. Üreteç sonsuz olduğu için, ondan kaç değer çıkaracağınızı kontrol etmeniz gerekir. Bu örnekte, ilk 10 değeri çıkarıyoruz.
Yineleyici Protokolünü Kullanmanın Faydaları
- Standardizasyon: Yineleyici Protokolü, farklı veri yapıları üzerinde yineleme yapmak için tutarlı bir yol sağlar.
- Esneklik: Özel ihtiyaçlarınıza göre özel yineleyiciler tanımlayabilirsiniz.
- Okunabilirlik:
for...of
döngüsü, yineleme kodunu daha okunabilir ve kısa hale getirir. - Verimlilik: Yineleyiciler tembel olabilir, yani yalnızca gerektiğinde değer üretirler, bu da büyük veri setleri için performansı artırabilir. Örneğin, yukarıdaki Fibonacci dizisi üreteci yalnızca `next()` çağrıldığında bir sonraki değeri hesaplar.
- Uyumluluk: Yineleyiciler, spread sözdizimi ve yapı bozma gibi diğer JavaScript özellikleriyle sorunsuz bir şekilde çalışır.
İleri Düzey Yineleyici Teknikleri
Yineleyicileri Birleştirme
Birden çok yineleyiciyi tek bir yineleyicide birleştirebilirsiniz. Bu, birden çok kaynaktan gelen verileri birleşik bir şekilde işlemeniz gerektiğinde kullanışlıdır.
function* combineIterators(...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
yield item;
}
}
}
const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";
const combined = combineIterators(array1, array2, string1);
for (const value of combined) {
console.log(value); // Çıktı: 1, 2, 3, a, b, c, X, Y, Z
}
Bu örnekte, `combineIterators` fonksiyonu argüman olarak herhangi bir sayıda yinelenebilir alır. Her bir yinelenebilir üzerinde döner ve her bir öğeyi yield eder. Sonuç, tüm girdi yinelenebilirlerinden gelen tüm değerleri üreten tek bir yineleyicidir.
Yineleyicileri Filtreleme ve Dönüştürme
Ayrıca, başka bir yineleyici tarafından üretilen değerleri filtreleyen veya dönüştüren yineleyiciler de oluşturabilirsiniz. Bu, verileri bir boru hattında işlemenize, her değere oluşturuldukça farklı işlemler uygulamanıza olanak tanır.
function* filterIterator(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* mapIterator(iterable, transform) {
for (const item of iterable) {
yield transform(item);
}
}
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);
for (const value of squaredEvenNumbers) {
console.log(value); // Çıktı: 4, 16, 36
}
Burada, `filterIterator` bir yinelenebilir ve bir yüklem fonksiyonu alır. Yalnızca yüklemin `true` döndürdüğü öğeleri yield eder. `mapIterator` bir yinelenebilir ve bir dönüştürme fonksiyonu alır. Her öğeye dönüştürme fonksiyonunu uygulamanın sonucunu yield eder.
Gerçek Dünya Uygulamaları
Yineleyici Protokolü, JavaScript kütüphanelerinde ve çerçevelerinde yaygın olarak kullanılır ve özellikle büyük veri setleri veya asenkron işlemlerle uğraşırken çeşitli gerçek dünya uygulamalarında değerlidir.
- Veri İşleme: Yineleyiciler, büyük veri setlerini verimli bir şekilde işlemek için kullanışlıdır, çünkü tüm veri setini belleğe yüklemeden verilerle parçalar halinde çalışmanıza olanak tanır. Müşteri verilerini içeren büyük bir CSV dosyasını ayrıştırdığınızı hayal edin. Bir yineleyici, tüm dosyayı bir kerede belleğe yüklemeden her satırı işlemenize izin verebilir.
- Asenkron İşlemler: Yineleyiciler, bir API'den veri getirme gibi asenkron işlemleri yönetmek için kullanılabilir. Veri mevcut olana kadar yürütmeyi duraklatmak ve ardından bir sonraki değerle devam etmek için üreteç fonksiyonlarını kullanabilirsiniz.
- Özel Veri Yapıları: Yineleyiciler, belirli gezinme gereksinimleri olan özel veri yapıları oluşturmak için gereklidir. Bir ağaç veri yapısı düşünün. Ağacı belirli bir sırada (örneğin, derinlemesine öncelikli veya genişlemesine öncelikli) gezmek için özel bir yineleyici uygulayabilirsiniz.
- Oyun Geliştirme: Oyun geliştirmede, yineleyiciler oyun nesnelerini, parçacık efektlerini ve diğer dinamik öğeleri yönetmek için kullanılabilir.
- Kullanıcı Arayüzü Kütüphaneleri: Birçok UI kütüphanesi, temel alınan veri değişikliklerine göre bileşenleri verimli bir şekilde güncellemek ve render etmek için yineleyicileri kullanır.
En İyi Uygulamalar
Symbol.iterator
'ı Doğru Uygulayın:Symbol.iterator
metodunuzun Yineleyici Protokolü'ne uyan bir yineleyici nesnesi döndürdüğünden emin olun.done
Bayrağını Doğru Yönetin:done
bayrağı, yinelemenin sonunu işaret etmek için kritik öneme sahiptir.next()
metodunuzda doğru şekilde ayarladığınızdan emin olun.- Üreteç Fonksiyonlarını Kullanmayı Düşünün: Üreteç fonksiyonları, yineleyiciler oluşturmak için daha kısa ve okunabilir bir yol sağlar.
next()
'te Yan Etkilerden Kaçının:next()
metodu öncelikle bir sonraki değeri almaya ve yineleyicinin durumunu güncellemeye odaklanmalıdır.next()
içinde karmaşık işlemler veya yan etkiler yapmaktan kaçının.- Yineleyicilerinizi Kapsamlı Bir Şekilde Test Edin: Özel yineleyicilerinizin doğru davrandığından emin olmak için farklı veri setleri ve senaryolarla test edin.
Sonuç
JavaScript Yineleyici Protokolü, veri yapılarında gezinmek için güçlü ve esnek bir yol sağlar. Yinelenebilir ve Yineleyici protokollerini anlayarak ve üreteç fonksiyonlarından yararlanarak, özel ihtiyaçlarınıza göre özel yineleyiciler oluşturabilirsiniz. Bu, verilerle verimli bir şekilde çalışmanıza, kod okunabilirliğini artırmanıza ve uygulamalarınızın performansını yükseltmenize olanak tanır. Yineleyicilerde ustalaşmak, JavaScript'in yeteneklerinin daha derin bir şekilde anlaşılmasını sağlar ve daha zarif ve verimli kod yazmanıza olanak tanır.